﻿using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;

namespace QbservableCore.Serialization
{
    /// <summary>
    /// Handle expression serialization from and to Json
    /// </summary>
    public sealed class ExpressionSerializer : IQbservableConvention
    {
        private static readonly MethodInfo _castToObjectMethodCache;

        public JsonSerializerSettings JsonSettings { get; private set; }

        #region Ctor

        static ExpressionSerializer()
        {
            var t = typeof(ExpressionSerializer);
            _castToObjectMethodCache = t.GetMethod("ToObject", BindingFlags.NonPublic | BindingFlags.Static);
        }

        public ExpressionSerializer()
        {
            JsonSettings = new JsonSerializerSettings();
            JsonSettings.TypeNameHandling = TypeNameHandling.All;
            JsonSettings.NullValueHandling = NullValueHandling.Ignore;
            JsonSettings.Context = new StreamingContext(StreamingContextStates.All, this);
        }

        #endregion // Ctor

        #region ContractResolver

        /// <summary>
        /// Gets or sets the contract resolver.
        /// can be used to rename property names
        /// </summary>
        /// <value>
        /// The contract resolver.
        /// </value>
        public IContractResolver ContractResolver
        {
            get { return JsonSettings.ContractResolver; }
            set { JsonSettings.ContractResolver = value; }
        }

        #endregion // ContractResolver

        #region AddConverters

        /// <summary>
        /// Adds the converters.
        /// can be used to modify the value of a property
        /// </summary>
        /// <param name="convertors"></param>
        public void AddConverters(params JsonConverter[] convertors)
        {
            foreach (var convertor in convertors)
            {
                JsonSettings.Converters.Add(convertor);
            }
        }

        #endregion // AddConverters

        #region Binder

        /// <summary>
        /// Gets or sets the binder.
        /// can be used to rename the type name
        /// </summary>
        /// <value></value>
        public SerializationBinder Binder
        {
            get { return JsonSettings.Binder; }
            set { JsonSettings.Binder = value; }
        }

        #endregion // Binder

        #region TargetResolver

        /// <summary>
        /// Gets or sets the target resolver.
        /// </summary>
        /// <value>
        /// The target resolver.
        /// </value>
        public IQbservableTargetResolver TargetResolver { get; set; }

        #endregion // TargetResolver

        #region ToJson

        /// <summary>
        /// Convert expression to Json
        /// </summary>
        /// <param name="instance">The instance.</param>
        /// <returns></returns>
        public string ToJson(Expression instance)
        {
            ExpressionNodeBuilder translator = new ExpressionNodeBuilder();
            ExpRepresentationBase node = translator.Build(instance);

            string json = JsonConvert.SerializeObject(node, JsonSettings);
            return json;
        }

        #endregion // ToJson

        public IObservable<object> ToObservable(string json)
        {
            if (TargetResolver == null)
            {
                throw new ArgumentException("TargetResolver can't be null");
            }
            ExpRepresentationBase m = JsonConvert.DeserializeObject<ExpRepresentationBase>
                (json, JsonSettings);

            ExepressionBuilderCache exepressionBuilderCache = new ExepressionBuilderCache();
            var exp = m.ToExpression(exepressionBuilderCache);

            var qb = AsObservableObject(ref exp);

            return qb;
        }

        /// <summary>
        /// Convert Expression of IQbservable<T> To IObservable<Object>
        /// </summary>
        /// <param name="exp">The expression</param>
        /// <returns></returns>
        private static IObservable<object> AsObservableObject(ref Expression exp)
        {
            //extract the Observable type
            Type observedType = exp.Type.GetObservedType();

            var castMethod = _castToObjectMethodCache.MakeGenericMethod(observedType);
            exp = Expression.Call(null, castMethod, exp);
            Expression<Func<object>> typedExpression = Expression.Lambda<Func<object>>(exp);
            Func<object> factory = typedExpression.Compile();

            var qb = factory() as IObservable<object>;
            return qb;
        }

        private static IObservable<object> ToObject<T>(IObservable<T> source)
        {
            return source.Select(item => (object)item);
        }
    }
}
